Skip to content

fix(ListView): normalize filter AST, safe export, request debounce, data limit warning#659

Merged
hotlong merged 3 commits intomainfrom
copilot/fix-listview-component-issues
Feb 20, 2026
Merged

fix(ListView): normalize filter AST, safe export, request debounce, data limit warning#659
hotlong merged 3 commits intomainfrom
copilot/fix-listview-component-issues

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 20, 2026

Addresses the 2026-02 ListView full scan findings: backend-incompatible in operator in filter AST, unsafe CSV export for complex types, race conditions from parallel data fetches, hardcoded $top=100 with no user feedback, and missing test coverage.

Filter normalization (Issues #1, #2)

  • normalizeFilterCondition() rewrites ['field', 'in', ['a','b']]['or', ['field','=','a'], ['field','=','b']] (and not inand of !=)
  • Applied to both convertFilterGroupToAST and userFilterConditions before merging
  • Filter merge now strips empty/invalid conditions before combining with and
normalizeFilterCondition(['status', 'in', ['active', 'pending']])
// → ['or', ['status', '=', 'active'], ['status', '=', 'pending']]

Export type safety (Issue #3)

  • Arrays serialize as semicolon-separated values (admin; user) instead of [object Object]
  • Objects serialize as JSON strings
  • Null/undefined → empty string

Request debounce (Issue #5)

  • fetchRequestIdRef counter ensures only the latest request's result is applied to state — stale responses from superseded requests are discarded

PullToRefresh lock prevention (Issue #6)

  • Pull distance and startYRef reset immediately before async refresh, preventing UI lock if the network is slow

Configurable $top + data limit warning (Issue #7)

  • Page size now reads from schema.pagination.pageSize (default 100)
  • Shows amber warning in status bar when returned items ≥ page size

i18n fallback expansion (Issue #8)

  • Added fallback translations for list.noItems, list.search, list.filter, list.sort, list.export, list.hideFields, list.pullToRefresh, list.refreshing, list.dataLimitReached

Defensive field handling (Issue #9)

  • Null-safe field name extraction in effectiveFields memo

Tests (Issue #10)

  • FilterNormalization.test.ts — 20 tests covering in/not in normalization, passthrough, logical group recursion, edge cases
  • Export.test.tsx — CSV complex type serialization, JSON export
  • DataFetch.test.tsx — data limit warning, custom page size, stale request guard
Original prompt

This section details on the original issue you should resolve

<issue_title>ListView 组件全量问题扫描(2026.02)</issue_title>
<issue_description>根据2026年2月代码全面扫描 ListView 存在的潜在/遗留问题,归纳如下:

1. userFilter 传递 in 操作符后端无效,AST 不兼容

  • UserFilters 传出的["field", "in", [...]]条件组合经常无法被后端识别,表现为过滤器选择无效。
  • 合并至 allFilters 时缺乏向后端兼容的转换,例如应拆解为 or 或多个 =[...]

2. filters/quickFilters/userFilters 多重条件合并缺乏类型与格式安全保障

  • allFilters 直接展开多个二维、三维结构易导致最终finalFilter不合预期。
  • 建议每个条件都做标准化处理,只有一条时也包裹成 and。

3. 导出 Export 仅支持 csv/json,缺少 xlsx/pdf,并且未做导出字段类型安全校验

  • 导出时未判断字段是否为复杂对象、数组类型,导出内容缺失或 csv 断行。

4. 行内编辑与 conditionalFormatting、hiddenFields 有协作冲突

  • 开启 inlineEdit 后,同步隐藏字段或应用条件样式时,UI 偶发异常。
  • 建议所有状态切换、过滤 hiddenFields 时及时刷新渲染 grid

5. filters/sorts/search 状态切换/重置后未做 loading 状态防抖

  • 修改快速切换 filter/sort 时多次请求并行,loading 状态无法精准控制,终端数据 display 不确定。
  • 建议所有异步请求唯一化、设置 min loading time、最后一次为准

6. PullToRefresh 手机端体验在分页/网络慢情况下容易卡死

  • 下拉刷新时 loading/数据切换未做异步锁,极易丢请求、锁死 UI

7. 大数据量虚拟滚动、分页 loadMore 等一页100条后无后继加载/无提示

  • 前端参数 $top 写死最大100,实际数据量大于100时 UI silent fail、无分页/无 loadMore/无提醒

8. i18n 回退逻辑存在硬编码&fallback到英文

  • 只覆盖了 'list.recordCount',其他自定义 label fallback 异步/局部回退表现欠佳

9. 只读 objectDef、schema.fields 不一致时,部分功能(如隐藏列/字段顺序等)报错

10. 单元测试缺失(Filters、UserFilters、Export���PullToRefresh、EmptyState等缺乏回归/交互测试覆盖)


建议优化方向:

  • 统一 filter AST 标准和转换、与后端严格对齐,建立完善的 filter 测试用例,并兼容后端协议。
  • 导出功能循环支持 xlsx/pdf、字符串转义、类型判断。
  • 加强 loading 管理和并发防抖。
  • 大数据下分页/滚动和空状态体验。
  • 完善交互行为单元测试。

本 issue 由 code scan & 案例复现归纳(2026/02),如有任何遗漏请评论补充。</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 20, 2026 1:38pm
objectui-demo Ready Ready Preview, Comment Feb 20, 2026 1:38pm
objectui-storybook Ready Ready Preview, Comment Feb 20, 2026 1:38pm

Request Review

Copilot AI and others added 2 commits February 20, 2026 13:16
…warning, i18n fallback

- Issue #1: Normalize `in`/`not in` operators to backend-compatible `or`/`and` of `=`/`!=`
- Issue #2: Filter merging now validates and filters empty conditions
- Issue #3: CSV export safely serializes arrays (semicolon-separated) and objects (JSON)
- Issue #5: Request counter prevents stale data from overwriting latest results
- Issue #6: PullToRefresh resets pull distance immediately to prevent UI lock
- Issue #7: $top configurable via schema.pagination, data limit warning shown
- Issue #8: Extended i18n fallback translations for all ListView labels
- Issue #9: Defensive null checks in effectiveFields for mismatched objectDef
- Issue #10: Added FilterNormalization, Export, and DataFetch test suites

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…PullToRefresh pattern

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix potential issues in ListView component from February 2026 scan fix(ListView): normalize filter AST, safe export, request debounce, data limit warning Feb 20, 2026
Copilot AI requested a review from hotlong February 20, 2026 13:23
@hotlong hotlong marked this pull request as ready for review February 20, 2026 13:38
Copilot AI review requested due to automatic review settings February 20, 2026 13:38
@hotlong hotlong merged commit 850b974 into main Feb 20, 2026
5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses ListView data fetching and filtering issues identified in a February 2026 code audit. It implements backend-compatible filter normalization, type-safe CSV export, request race condition prevention, pull-to-refresh reliability improvements, and configurable pagination with user warnings.

Changes:

  • Implements normalizeFilterCondition() and normalizeFilters() to convert in/not in operators to backend-compatible or/and of =/!= equality conditions
  • Adds type-safe CSV serialization for arrays (semicolon-separated) and objects (JSON-stringified) to prevent [object Object] in exports
  • Introduces fetchRequestIdRef counter to ensure only the latest data fetch updates UI state, preventing stale data from overwriting newer results
  • Fixes pull-to-refresh UI lock by resetting distance/startY before async refresh
  • Adds configurable schema.pagination.pageSize (default 100) with amber warning when data hits limit
  • Expands i18n fallback translations for list UI elements
  • Adds defensive null checks in field name extraction for effectiveFields

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/plugin-list/src/index.tsx Exports new normalizeFilterCondition and normalizeFilters functions for external use
packages/plugin-list/src/tests/FilterNormalization.test.ts Adds 20 tests covering in/not in normalization, passthrough, recursion, and edge cases
packages/plugin-list/src/tests/Export.test.tsx Adds 3 tests verifying CSV/JSON export with complex data types (arrays, objects, null)
packages/plugin-list/src/tests/DataFetch.test.tsx Adds 4 tests for data limit warning display, custom page size, and request debounce
packages/plugin-list/src/ListView.tsx Core implementation: filter normalization in data fetch, type-safe CSV export, request ID guard, configurable pagination, i18n translations, defensive field handling
packages/mobile/src/usePullToRefresh.ts Resets pull distance and startY immediately before async refresh to prevent UI lock
Comments suppressed due to low confidence (1)

packages/plugin-list/src/ListView.tsx:397

  • The setLoading(true) call at line 397 is not guarded by the request ID check. This means if multiple fetches fire in rapid succession (e.g., due to quick filter/sort changes), all requests will set loading to true, but only the latest request will set it to false (line 471).

While this is generally safe because React batches state updates and the latest request will eventually clear the loading state, consider moving the loading state management inside the request ID guard for consistency:

const requestId = ++fetchRequestIdRef.current;
if (requestId === fetchRequestIdRef.current) {
  setLoading(true);
}

Or accept that the current pattern is acceptable since any stale request that tries to clear loading will be blocked, and the latest request will eventually clear it.

  // Fetch data effect
  React.useEffect(() => {
    let isMounted = true;
    const requestId = ++fetchRequestIdRef.current;
    
    const fetchData = async () => {
      if (!dataSource || !schema.objectName) return;
      
      setLoading(true);

Comment on lines +120 to +159
it('only uses data from the latest request when multiple fetches occur', async () => {
let resolveFirst: (value: any) => void;
let resolveSecond: (value: any) => void;

const firstPromise = new Promise(resolve => { resolveFirst = resolve; });
const secondPromise = new Promise(resolve => { resolveSecond = resolve; });

mockDataSource.find
.mockReturnValueOnce(firstPromise)
.mockReturnValueOnce(secondPromise);

const schema: ListViewSchema = {
type: 'list-view',
objectName: 'contacts',
viewType: 'grid',
fields: ['name'],
};

const { rerender } = renderWithProvider(
<ListView schema={schema} dataSource={mockDataSource} />,
);

// Resolve second (newer) request first
resolveSecond!([{ _id: '2', name: 'Second' }]);

await vi.waitFor(() => {
expect(mockDataSource.find).toHaveBeenCalled();
});

// Resolve first (stale) request later
resolveFirst!([{ _id: '1', name: 'First (stale)' }]);

// Wait for state to settle — second request data should win
await vi.waitFor(() => {
// Data should eventually render from latest successful request
expect(screen.queryByTestId('empty-state')).not.toBeNull();
}, { timeout: 2000 }).catch(() => {
// This is fine — the key point is stale data doesn't overwrite new data
});
});
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request debounce test doesn't actually trigger two separate data fetches. The test creates two mock promises but only renders the ListView component once. Since the useEffect dependencies don't change between the two promise resolutions, only one fetch occurs.

To properly test the debounce mechanism, the test should either:

  1. Rerender the component with different filter/sort props to trigger a second fetch
  2. Or update state values that are in the useEffect dependency array (like currentFilters, currentSort, etc.)

Copilot uses AI. Check for mistakes.
expect(normalizeFilterCondition(input)).toEqual(
['or', ['or', ['priority', '=', 1], ['priority', '=', 2]], ['status', '=', 'active']],
);
});
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite is missing a test case for logical groups with a single child condition, e.g., ['and', ['status', '=', 'active']]. This edge case has only 2 elements, which means it would fail the length check at line 62 of normalizeFilterCondition and return early before reaching the recursion logic at line 67-70.

While this may be an uncommon case, it's worth adding a test to document the expected behavior - either the function should handle it gracefully or explicitly document that single-child logical groups are not supported.

Suggested change
});
});
it('handles single-child logical `and` group gracefully', () => {
const input = ['and', ['status', '=', 'active']] as any;
expect(normalizeFilterCondition(input)).toEqual([]);
});

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +190
'list.search': 'Search',
'list.filter': 'Filter',
'list.sort': 'Sort',
'list.export': 'Export',
'list.hideFields': 'Hide fields',
'list.showAll': 'Show all',
'list.pullToRefresh': 'Pull to refresh',
'list.refreshing': 'Refreshing…',
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several new i18n translation keys are defined but may not all be used: 'list.search', 'list.filter', 'list.sort', 'list.export', 'list.hideFields', 'list.showAll', 'list.pullToRefresh', 'list.refreshing'.

While 'list.dataLimitReached' is properly used (line 1151), the other newly added translations should be verified against their usage points. If they're not used in the changed code sections, consider whether they should be added in this PR or deferred to a future PR when those UI elements are actually implemented/updated.

Suggested change
'list.search': 'Search',
'list.filter': 'Filter',
'list.sort': 'Sort',
'list.export': 'Export',
'list.hideFields': 'Hide fields',
'list.showAll': 'Show all',
'list.pullToRefresh': 'Pull to refresh',
'list.refreshing': 'Refreshing…',

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +97
export function normalizeFilterCondition(condition: any[]): any[] {
if (!Array.isArray(condition) || condition.length < 3) return condition;

const [field, op, value] = condition;

// Recurse into logical groups
if (typeof field === 'string' && (field === 'and' || field === 'or')) {
return [field, ...condition.slice(1).map((c: any) =>
Array.isArray(c) ? normalizeFilterCondition(c) : c
)];
}

if (op === 'in' && Array.isArray(value)) {
if (value.length === 0) return [];
if (value.length === 1) return [field, '=', value[0]];
return ['or', ...value.map((v: any) => [field, '=', v])];
}

if (op === 'not in' && Array.isArray(value)) {
if (value.length === 0) return [];
if (value.length === 1) return [field, '!=', value[0]];
return ['and', ...value.map((v: any) => [field, '!=', v])];
}

return condition;
}

/**
* Normalize an array of filter conditions, expanding `in`/`not in` operators
* and ensuring consistent AST structure.
*/
export function normalizeFilters(filters: any[]): any[] {
if (!Array.isArray(filters) || filters.length === 0) return [];
return filters
.map(f => Array.isArray(f) ? normalizeFilterCondition(f) : f)
.filter(f => Array.isArray(f) && f.length > 0);
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter normalization functions use any[] for the condition parameter and return type. While this is pragmatic for heterogeneous filter AST structures, consider creating a type alias to make the intent clearer and improve maintainability:

type FilterCondition = any[]; // or more specific: [string, string, any] | [string, ...FilterCondition[]]

This would make the function signatures more self-documenting and allow for gradual type refinement in the future without breaking changes to the API.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ListView 组件全量问题扫描(2026.02)

3 participants